// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


/**
 * @fileoverview Define the controller for bite.layer.Config console.  The
 * object defines the functionality specifically for this controller and does
 * not affect the outside world.  However, it does define a set of signals that
 * can be used by external sources to perform actions when the controller
 * performs certain tasks.
 *
 * The Config console is intended to have only a single instance.
 *
 * @author jasonstredwick@google.com (Jason Stredwick)
 */


goog.provide('bite.layer.Config');
goog.provide('bite.layer.ConfigEventType');

goog.require('bite.common.events.DataBearingEvent');
goog.require('bite.common.mvc.helper');
goog.require('bite.layer.ConsoleBehavior');
goog.require('bite.layer.soy.Config');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');



/**
 * Constructs an instance of a Config controller.  The constructor creates the
 * object in a default state that has not been mapped to a view.
 * Note: As I need to use goog.mixin to fake multiple inheritance, I believe I
 *       can only extend the console once, so I am using EventTarget for that.
 * @extends {goog.events.EventTarget}
 * @constructor
 * @export
 */
bite.layer.Config = function() {
  goog.base(this);

  /**
   * The console element.
   * @type {Element}
   * @private
   */
  this.console_ = null;

  /**
   * Whether or not the console's view is initialized.
   * @type {boolean}
   * @private
   */
  this.isViewInitialized_ = false;

  /**
   * Whether or not the console's controller is initialized.
   * @type {boolean}
   * @private
   */
  this.isModelInitialized_ = false;

  /**
   * The tbody element of the table representing the layers.
   * @type {Element}
   * @private
   */
  this.tbody_ = null;
};
goog.inherits(bite.layer.Config, goog.events.EventTarget);
goog.mixin(bite.layer.Config.prototype, bite.layer.ConsoleBehavior.prototype);
goog.addSingletonGetter(bite.layer.Config);


/**
 * The console id.
 * @type {string}
 * @private
 */
bite.layer.Config.CONSOLE_ID_ = 'layer-config-console';


/**
 * Actions for external response.
 * @enum {string}
 * @private
 */
bite.layer.Config.Action_ = {
  EDIT: 'edit',
  REMOVE: 'remove'
};


/**
 * Button ids for the console.
 * @enum {string}
 * @export
 */
bite.layer.Config.Button = {
  ADD: 'layer-config-add',
  CLOSE: 'layer-config-close',
  CREATE: 'layer-config-create'
};


/**
 * Defines key values used to pass data in and out of the console.
 * @enum {string}
 * @export
 */
bite.layer.Config.Data = {
  ACTION: 'action',
  NAME: 'name',
  URL: 'url',
  VALUE: 'value'
};


/**
 * Input buttons ids for the console.
 * @enum {string}
 * @private
 */
bite.layer.Config.InputBase_ = {
  EDIT: 'layer-config-edit-',
  REMOVE: 'layer-config-remove',
  REMOVE_WITH_NAME: 'layer-config-remove-'
};


/**
 * Element ids for the console.
 * @enum {string}
 * @private
 */
bite.layer.Config.Table_ = {
  NAME: 'layer-config-name',
  TBODY: 'layer-config-tbody',
  TR: 'layer-config-tr',
  TR_WITH_NAME: 'layer-config-tr-'
};


/**
 * Event types supported by this console.
 * @enum {string}
 * @export
 */
bite.layer.ConfigEventType = {
  ADD: 'add',
  CLOSE: 'close',
  CREATE: 'create',
  EDIT: 'edit',
  REMOVE: 'remove'
};


/**
 * Create a new row element with the given name.
 * @param {string} url The url of the layer.
 * @param {string=} opt_name An optional name for the layer.
 * @return {Element} The new element or null.
 * @private
 */
bite.layer.Config.prototype.createRow_ = function(url, opt_name) {
  var row = goog.dom.createElement(goog.dom.TagName.TR);
  if (!row) {
    return null;
  }

  row.id = goog.isDef(opt_name) ?
      bite.layer.Config.Table_.TR_WITH_NAME + opt_name :
      bite.layer.Config.Table_.TR;

  // The row must be appended into a table prior to rendering or the browser
  // will strip out all table related tags.
  var tbody = /** @type {!Element} */ (this.tbody_);
  goog.dom.appendChild(tbody, row);

  var data = {};
  data[bite.layer.Config.Data.URL] = url;
  if (goog.isDef(opt_name)) {
    data[bite.layer.Config.Data.NAME] = opt_name;
  }

  var helper = bite.common.mvc.helper;
  if (!helper.renderModelFor(row, bite.layer.soy.Config.createRow, data)) {
    goog.dom.removeNode(/** @type {Node} */ (row));
    return null;
  }

  // Elements with no name will have their edit button disabled.  Also,
  // when the edit button is disabled it will have no element matching the
  // tested id, thus getElement will return null.
  var editName = bite.layer.Config.InputBase_.EDIT + opt_name;
  var edit = helper.getElement(editName, row);
  if (!goog.isNull(edit)) {
    goog.events.listen(edit, goog.events.EventType.CLICK,
                       goog.bind(this.handleOnEdit_, this, opt_name));
  }

  var removeId = goog.isDef(opt_name) ?
      bite.layer.Config.InputBase_.REMOVE_WITH_NAME + opt_name :
      bite.layer.Config.InputBase_.REMOVE;
  var remove = helper.getElement(removeId, row);
  if (!goog.isNull(remove)) {
    goog.events.listen(remove, goog.events.EventType.CLICK,
                       goog.bind(this.handleOnRemove_, this, url, opt_name));
  }

  return row;
};


/**
 * Return a row from the configuration table if it exists.  It will lookup the
 * row based on a name if given or by url if name is not given.
 * @param {string} url The layer url.
 * @param {string=} opt_name An optional name to look up the row by.
 * @return {Element} Returns either the element or null if not found.
 * @private
 */
bite.layer.Config.prototype.findRow_ = function(url, opt_name) {
  var tbody = /** @type {!Element} */ (this.tbody_);
  var row = null;

  if (goog.isDef(opt_name)) {
    var rowId = bite.layer.Config.Table_.TR_WITH_NAME + opt_name;
    // TODO(jasonstredwick): At some point rethink getElement in case there are
    // more than one entry.  This should never be the case but if it is then
    // the issue would be compounded by just adding more.
    row = bite.common.mvc.helper.getElement(rowId, tbody);
  } else {
    // TODO (jasonstredwick): Examine adding a multiple find by id.
    var rows = tbody.querySelectorAll('#' + bite.layer.Config.Table_.TR);
    for (var i = 0, len = rows.length; i < len; ++i) {
      var possibleRow = rows[i];
      // TODO (jasonstredwick): Add more robust selection.
      var nameCol = possibleRow.querySelector(
          '.' + bite.layer.Config.Table_.NAME);
      if (nameCol && nameCol.innerHTML == url) {
        row = possibleRow;
        break;
      }
    }
  }

  return row;
};


/**
 * Handler that is called when the add url button is pressed.
 * @private
 */
bite.layer.Config.prototype.handleOnAddUrl_ = function() {
  var type = bite.layer.ConfigEventType;
  var eventOut = new bite.common.events.DataBearingEvent(type.ADD);
  this.dispatchEvent(eventOut);
};


/**
 * Handler that is called when the close button is pressed.
 * @private
 */
bite.layer.Config.prototype.handleOnClose_ = function() {
  var type = bite.layer.ConfigEventType;
  var eventOut = new bite.common.events.DataBearingEvent(type.CLOSE);
  this.dispatchEvent(eventOut);
};


/**
 * Handler that is called when the create button is pressed.
 * @private
 */
bite.layer.Config.prototype.handleOnCreate_ = function() {
  var type = bite.layer.ConfigEventType;
  var eventOut = new bite.common.events.DataBearingEvent(type.CREATE);
  this.dispatchEvent(eventOut);
};


/**
 * Handler that is called when the edit button is pressed.
 * @param {string} name The name associated with the pressed button.
 * @private
 */
bite.layer.Config.prototype.handleOnEdit_ = function(name) {
  var data = {};
  data[bite.layer.Config.Data.NAME] = name;
  data[bite.layer.Config.Data.ACTION] = bite.layer.Config.Action_.EDIT;

  var type = bite.layer.ConfigEventType;
  var eventOut = new bite.common.events.DataBearingEvent(type.EDIT, data);
  this.dispatchEvent(eventOut);
};


/**
 * Handler that is called when the remove button is pressed.
 * @param {string} url The url of the layer.
 * @param {string|undefined} name The name associated with the pressed button.
 * @private
 */
bite.layer.Config.prototype.handleOnRemove_ = function(url, name) {
  var data = {};
  if (name) {
    data[bite.layer.Config.Data.NAME] = name;
  }
  data[bite.layer.Config.Data.URL] = url;
  data[bite.layer.Config.Data.ACTION] = bite.layer.Config.Action_.REMOVE;
  console.log('name: ' + name);
  console.log('url: ' + url);

  var type = bite.layer.ConfigEventType;
  var eventOut = new bite.common.events.DataBearingEvent(type.REMOVE, data);
  this.dispatchEvent(eventOut);
};


/**
 * Initialize the Config Console by creating the model and hooking
 * functions to the model.  If a baseUrl is given and the view is not yet
 * initialized then this function will try to initialize it.  Nothing happens
 * if the console is already initialized.
 * @param {string=} opt_baseUrl The baseUrl required for relative references.
 * @return {boolean} Whether or not the model is initialized.  It will also
 *     return true if the model is already initialized.
 * @export
 */
bite.layer.Config.prototype.init = function(opt_baseUrl) {
  if (goog.isDef(opt_baseUrl) && !this.isViewInitialized_) {
    this.initView(opt_baseUrl);
  }

  if (this.isModelInitialized_) {
    return true;
  }

  var helper = bite.common.mvc.helper;
  this.console_ = helper.initModel(bite.layer.soy.Config.getModel);
  if (this.console_ &&
      this.setupBehavior_(this.console_) &&
      this.setupButtonHandlers_(this.console_) &&
      this.setupUsefulElements_(this.console_)) {
    this.isModelInitialized_ = true;
  } else {
    this.undoSetup_();
  }

  return this.isModelInitialized_;
};


/**
 * Given an optional baseUrl, initialize the view for the console by adding
 * its css to the page.
 * @param {string=} baseUrl The baseUrl required for relative references.
 * @return {boolean} Whether or not the view was added.  It also returns true
 *     if the view is already initialized.
 * @export
 */
bite.layer.Config.prototype.initView = function(baseUrl) {
  var helper = bite.common.mvc.helper;
  var config = bite.layer.soy.Config;
  if (!this.isViewInitialized_ && helper.initView(config.getView, baseUrl)) {
    this.isViewInitialized_ = true;
  }

  return this.isViewInitialized_;
};


/**
 * Determines whether or not the console is initialized and ready for use.
 * @return {boolean} See function comment.
 * @export
 */
bite.layer.Config.prototype.isInitialized = function() {
  return this.isViewInitialized_ && this.isModelInitialized_;
};


/**
 * Remove the specified element.
 * @param {string} url The url of the layer.
 * @param {string=} opt_name An optional name of the layer.
 */
bite.layer.Config.prototype.remove = function(url, opt_name) {
  goog.dom.removeNode(this.findRow_(url, opt_name));
};


/**
 * Complete rebuild the table with the given data.
 * @param {Array.<{name: string, url: string}>=} data The data used
 *     to update the row, the name is optional.
 * @return {boolean} Whether or not the console was updated.
 * @export
 */
bite.layer.Config.prototype.reset = function(data) {
  if (!this.isInitialized()) {
    return false;
  }

  // Clear table
  this.tbody_.innerHTML = '';

  // Repopulate table
  var len = (data && data.length) ? data.length : 0;
  for (var i = 0; i < len; ++i) {
    if (!this.update(data[i])) {
      this.tbody_.innerHTML = '';
      return false;
    }
  }

  return true;
};


/**
 * Setup an element to be controlled by the behaviors.
 * @param {!Element} srcElement The element to control.
 * @return {boolean} Setup was successful.
 * @private
 */
bite.layer.Config.prototype.setupBehavior_ = function(srcElement) {
  var obj = /** @type {Object} */ (this);
  obj.setBehavior(srcElement);
  obj.hide();
  obj.disable();

  return true;
};


/**
 * Setup buttons in console.
 * @param {!Element} srcElement The element to search from.
 * @return {boolean} Whether or not the button handlers were setup correctly.
 * @private
 */
bite.layer.Config.prototype.setupButtonHandlers_ = function(srcElement) {
  var helper = bite.common.mvc.helper;
  var button = bite.layer.Config.Button;

  var add = helper.getElement(button.ADD, srcElement);
  goog.events.listen(add, goog.events.EventType.CLICK,
                     goog.bind(this.handleOnAddUrl_, this));

  var close = helper.getElement(button.CLOSE, srcElement);
  goog.events.listen(close, goog.events.EventType.CLICK,
                     goog.bind(this.handleOnClose_, this));

  var create = helper.getElement(button.CREATE, srcElement);
  goog.events.listen(create, goog.events.EventType.CLICK,
                     goog.bind(this.handleOnCreate_, this));

  return true;
};


/**
 * Locate useful elements within the DOM and record their Ref for later use.
 * @param {!Element} srcElement The element to search from.
 * @return {boolean} Whether or not the element handlers were setup correctly.
 * @private
 */
bite.layer.Config.prototype.setupUsefulElements_ = function(srcElement) {
  this.tbody_ = bite.common.mvc.helper.getElement(
      bite.layer.Config.Table_.TBODY, srcElement);

  return true;
};


/**
 * Reset the model portion of the controller back to an uninitialized state.
 * @private
 */
bite.layer.Config.prototype.undoSetup_ = function() {
  // Make sure and remove the DOM Element from the document upon failure.
  if (this.console_) {
    bite.common.mvc.helper.removeElementById(bite.layer.Config.CONSOLE_ID_);
  }

  this.tbody_ = null;
  this.console_ = null;
  this.isModelInitialized_ = false;
};


/**
 * Using a single set of data, update the associated row.
 * @param {{name: string, url: string}} data The data used to update the row,
 *     name is optional.
 * @return {boolean} Whether or not the console was updated.
 * @export
 */
bite.layer.Config.prototype.update = function(data) {
  if (!this.isInitialized() || !this.validateRowData_(data)) {
    return false;
  }

  var name = data[bite.layer.Config.Data.NAME];
  var url = data[bite.layer.Config.Data.URL];

  // The row element must be created manually rather than letting soy create
  // it because soy uses DIVs by default and setting the innerHTML of a DIV
  // to a TR element will remove all table related tags from the rendered
  // output.
  var row = this.findRow_(url, name);
  if (row) {
    // There is nothing to update currently.
    return true;
  }

  row = this.createRow_(url, name);
  if (!row) {
    return false;
  }

  return true;
};


/**
 * Validate the data for a single row, as it is coming from an external
 * source.
 * @param {{name: string}} data The data to be validated.
 * @return {boolean} Whether or not the data is valid.
 * @private
 */
bite.layer.Config.prototype.validateRowData_ = function(data) {
  if (!data) {
    return false;
  }

  if (!(bite.layer.Config.Data.URL in data)) {
    return false;
  }

  if (bite.layer.Config.Data.NAME in data) {
    var name = data[bite.layer.Config.Data.NAME];
    if (!bite.common.mvc.helper.validateName(name)) {
      return false;
    }
  }

  return true;
};

